package com.wxtwxt.ruralmall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.wxtwxt.ruralmall.product.vo.Catelog2Vo;
import org.apache.commons.lang.StringUtils;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wxtwxt.ruralmall.common.utils.PageUtils;
import com.wxtwxt.ruralmall.common.utils.Query;

import com.wxtwxt.ruralmall.product.dao.CategoryDao;
import com.wxtwxt.ruralmall.product.entity.CategoryEntity;
import com.wxtwxt.ruralmall.product.service.CategoryService;

import javax.annotation.Resource;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        List<CategoryEntity> allList = this.list();
        return allList.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .map(category -> {
                    category.setChildren(getChildren(category, allList));
                    return category;
                })
                .sorted((menu1, menu2) ->
                        (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()))
                .collect(Collectors.toList());
    }

    @Override
    public void removeMenusByIds(List<Long> asList) {
        // TODO 检查当前删除的菜单，是否被其他地方引用
        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> path = new ArrayList<>();
        List<Long> catelogPath = findPath(catelogId, path);
        Collections.reverse(catelogPath);
        return catelogPath.toArray(new Long[0]);
    }

    /**
     * 1.每一个需要缓存的数据我们都来指定要放到那个名字的缓存 【缓存的分区（按照业务类型）】
     * 2.@Cacheable(value = "category")
     *      代表当前方法的结果需要缓存，如果缓存中有，方法不在调用
     *      如果缓存中没有，会调用方法，最后将方法的结果放入缓存
     *3.默认行为：
     *      1) 如果缓存中有，方法不在调用
     *      2) key默认自动生成，缓存的名字::SimpleKey {}(自主生成的key值)
     *      3) 缓存的value的值，默认使用jdk序列化机制，将序列化后的数据存到redis
     *      4) 默认ttl时间为-1
     *
     *      自定义：
     *          1) 指定生成的缓存使用的key；key属性指定，接受一个SpEl
     *          2) 指定缓存的数据的存活时间  配置文件设置
     *          3) 将数据保存为json格式
     *
     */

    @Cacheable(value = "category", key = "#root.methodName")
    @Override
    public List<CategoryEntity> findLevel1CateGorys() {
        QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("parent_cid", 0);
        return this.list(queryWrapper);
    }

    @Cacheable(value = "category", key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCateLogJson() {
        List<CategoryEntity> allCategoryList = this.list(null);
        // 查询所有的一级分类
        List<CategoryEntity> level1CateGorys = findByParentCid(allCategoryList, 0L);
        Map<String, List<Catelog2Vo>> collectData = level1CateGorys.stream().collect(Collectors.toMap((item -> item.getCatId().toString()), v -> {
            List<CategoryEntity> level2List = findByParentCid(allCategoryList, v.getCatId());
            List<Catelog2Vo> catelog2VoList = new ArrayList<>();
            if (level2List.size() > 0) {
                catelog2VoList = level2List.stream().map(level2Item -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo();
                    catelog2Vo.setCatalog1Id(v.getCatId().toString());
                    List<CategoryEntity> level3List = findByParentCid(allCategoryList, level2Item.getCatId());
                    List<Catelog2Vo.Catelog3Vo> catelog3Vos = new ArrayList<>();
                    if (level3List.size() > 0) {
                        catelog3Vos = level3List.stream().map(level3Item -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
                            catelog3Vo.setCatalog2Id(level2Item.getCatId().toString());
                            catelog3Vo.setId(level3Item.getCatId().toString());
                            catelog3Vo.setName(level3Item.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                    }
                    catelog2Vo.setCatalog3List(catelog3Vos);
                    catelog2Vo.setId(level2Item.getCatId().toString());
                    catelog2Vo.setName(level2Item.getName());
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2VoList;
        }));
        return collectData;
    }

    // TODO 产生堆外内存溢出：OutOfDirectMemoryError
    // 1.SpringBoot2.0以后，默认使用lettuce作为操作redis的客户端，它使用netty进行网络通信
    // 2.lettuce的bug导致堆外内存溢出，netty如果没有指定堆外内存，默认使用-Xmx300m
    // 解决方案：不能使用-Dio.netty.maxDirectMemory只去调大堆外内存
    // 1.升级netty客户端 2.切换使用jedis
    public Map<String, List<Catelog2Vo>> getCateLogJson2() {
        /**
         * 1.空结果缓存，解决缓存穿透问题
         * 2.设置过期时间（加随机值），解决缓存雪崩
         * 3.加锁，解决缓存击穿问题
         */
        // 判断缓存中是否有数据
        String data = stringRedisTemplate.opsForValue().get("catelogJson");
        if (StringUtils.isEmpty(data)) {
            // redis没有数据，从数据库中查出数据进行返回，将数据转换成json格式存储到缓存中
            return getCateLogJsonByDbWithRedisLock();
        }
        return JSON.parseObject(data, new TypeReference<Map<String, List<Catelog2Vo>>>() {});
    }

    private Map<String, List<Catelog2Vo>> getCateLogJsonByDbWithRedisLock() {
        // 1.占分布式锁
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(lock)) {
            // 加锁成功
            // 设置过期时间，必须和加锁是同步的，原子操作
//            stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS);
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            try {
                dataFromDb = getDataFromDb();
            } finally {
                // 获得值对比 + 对比值删除 = 原子操作  lua脚本解锁
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList("lock"), uuid);
            }
//            String lockValue = stringRedisTemplate.opsForValue().get("lock");
//            if(uuid.equals(lockValue)) {
//                stringRedisTemplate.delete("lock"); // 删除锁
//            }
            return dataFromDb;
        } else {
            // 加锁失败 重试
            // 休眠100ms重试
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return getCateLogJsonByDbWithRedisLock(); // 自旋的方式
        }

    }

    private Map<String, List<Catelog2Vo>> getCateLogJsonByDbWithLocalLock() {
        // 只要是同一把锁，就能锁住需要这个锁的全部线程
        // 1.synchronized (this) ：SpringBoot所有的组件在容器中都是单例的
        // TODO 本地锁：synchronized，JUC，在分布式情况下，想要锁住所有，必须使用分布式锁
        synchronized (this) {
            // 得到锁以后，我们应该先去缓存中确定一次，如果没有再查数据库
            return getDataFromDb();
        }
    }

    private Map<String, List<Catelog2Vo>> getDataFromDb() {
//        String catelogJson = stringRedisTemplate.opsForValue().get("catelogJson");
        // TODO 优化查询
        List<CategoryEntity> allCategoryList = this.list(null);
        // 查询所有的一级分类
        List<CategoryEntity> level1CateGorys = findByParentCid(allCategoryList, 0L);
        Map<String, List<Catelog2Vo>> collectData = level1CateGorys.stream().collect(Collectors.toMap((item -> item.getCatId().toString()), v -> {
            List<CategoryEntity> level2List = findByParentCid(allCategoryList, v.getCatId());
            List<Catelog2Vo> catelog2VoList = new ArrayList<>();
            if (level2List.size() > 0) {
                catelog2VoList = level2List.stream().map(level2Item -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo();
                    catelog2Vo.setCatalog1Id(v.getCatId().toString());
                    List<CategoryEntity> level3List = findByParentCid(allCategoryList, level2Item.getCatId());
                    List<Catelog2Vo.Catelog3Vo> catelog3Vos = new ArrayList<>();
                    if (level3List.size() > 0) {
                        catelog3Vos = level3List.stream().map(level3Item -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
                            catelog3Vo.setCatalog2Id(level2Item.getCatId().toString());
                            catelog3Vo.setId(level3Item.getCatId().toString());
                            catelog3Vo.setName(level3Item.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                    }
                    catelog2Vo.setCatalog3List(catelog3Vos);
                    catelog2Vo.setId(level2Item.getCatId().toString());
                    catelog2Vo.setName(level2Item.getName());
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2VoList;
        }));
        stringRedisTemplate.opsForValue().set("catelogJson", JSON.toJSONString(collectData));
        return collectData;
    }

    private List<CategoryEntity> findByParentCid(List<CategoryEntity> allCategoryList, Long parentCid) {
        return allCategoryList.stream().filter(item -> Objects.equals(item.getParentCid(), parentCid)).collect(Collectors.toList());
    }

    private List<Long> findPath(Long catelogId, List<Long> path) {
        path.add(catelogId);
        CategoryEntity category = this.getById(catelogId);
        if (category.getParentCid() != 0) {
            findPath(category.getParentCid(), path);
        }
        return path;
    }

    /**
     * 获取子菜单
     *
     * @param category 当前菜单
     * @param allList  所有菜单
     */
    private List<CategoryEntity> getChildren(CategoryEntity category, List<CategoryEntity> allList) {
        return allList.stream()
                .filter(categoryEntity -> Objects.equals(categoryEntity.getParentCid(), category.getCatId()))
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getChildren(categoryEntity, allList));
                    return categoryEntity;
                })
                .sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()))
                .collect(Collectors.toList());
    }

}